package com.example.tys.baseapplication.net_tcp.frame.NettyImpl.tcp;

import android.util.Log;


import com.example.tys.baseapplication.net_tcp.frame.ConnectStatus;
import com.example.tys.baseapplication.net_tcp.frame.ListenCallback;
import com.example.tys.baseapplication.net_tcp.frame.core.ITcpClient;
import com.example.tys.baseapplication.net_tcp.frame.packet.BasePacket;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeoutException;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;


/**
 * Created by tys on 2019/4/30.
 */

abstract class AbstractTcpClient<PacketRequest extends BasePacket, PacketResponse extends BasePacket>
        implements ITcpClient<PacketRequest, PacketResponse> {

    private static final String TAG = "AbstractTcpClient";
    private Bootstrap mBootstrap;
    private NioEventLoopGroup mWorkGroup;
    private int CONNECT_TIMEOUT_MILLIS = 10000;
    private int READ_TIMEOUT_SECONDS = 0;
    private int WRITE_TIMEOUT_SECONDS = 0;
    private int ALL_IDLE_TIMEOUT_SECONDS = 60;
    private volatile ConnectStatus mConnectStatus = ConnectStatus.CONNECT_FAILED;
    private ChannelFuture mChannelFuture;
    private InetSocketAddress mInetSocketAddress;

    public AbstractTcpClient() {
        init();
    }

    private void init() {
        mBootstrap = new Bootstrap();
        mWorkGroup = new NioEventLoopGroup();
        setOptions();
    }

    private void setOptions() {
        mBootstrap.channel(NioSocketChannel.class)
                .group(mWorkGroup)
                .option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getConnectTimeout())
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        pipeline.addLast(new IdleStateHandler(getReadTimeout(), getWriteTimeout(), getAllIdleTimeout()));
                        for (Class<? super ChannelHandler> mEncoder : getEncodes()) {
                            pipeline.addLast((ChannelHandler) mEncoder.newInstance());
                        }
                        for (Class<? super ChannelHandler> mDecoder : getDecodes()) {
                            pipeline.addLast((ChannelHandler) mDecoder.newInstance());
                        }
                        pipeline.addLast("BaseHandler", new BaseHandler());
                        Class<? super ChannelHandler>[] handles = getHandles();
                        if (handles != null) {
                            for (Class<? super ChannelHandler> mHandler : handles) {
                                pipeline.addLast((ChannelHandler) mHandler.newInstance());
                            }
                        }
                    }
                });
    }

    @Override
    public PacketResponse send(PacketRequest packetRequest, int timeOutMillis) throws TimeoutException,
            InterruptedException, RejectedExecutionException, IOException {
        if (null == packetRequest) {
            throw new RejectedExecutionException("rejected send msg, msg is null");
        }
        if (beforeSend(packetRequest)) {
            throw new RejectedExecutionException(
                    "before sendSync, rejected sendSync msg " + (packetRequest.uniqueId
                            + " " + packetRequest.toString()));
        } else {
            channelSend(packetRequest);
            return afterSendAndWait(packetRequest, timeOutMillis);
        }
    }

    private void channelSend(PacketRequest packetRequest) throws IOException {
        if (mChannelFuture != null) {
            mChannelFuture.channel().writeAndFlush(packetRequest);
        } else {
            throw new IOException("channelFuture is null, determine whether the connection has been made and succeeded");
        }
    }

    @Override
    public void connect(InetSocketAddress inetSocketAddress, ListenCallback callback) {
        ConnectStatus currentStatus;
        if ((currentStatus = getConnectStatus()) == ConnectStatus.CONNECT_ING ||
                currentStatus == ConnectStatus.CONNECT_SUCCEED) {
            return;
        }
        setConnectStatus(ConnectStatus.CONNECT_ING, inetSocketAddress);
        ChannelFuture channelFuture;
        channelFuture = mBootstrap.connect(inetSocketAddress);
        channelFuture.addListener((ChannelFutureListener) future -> {
            if (future.isSuccess()) {
                setConnectStatus(ConnectStatus.CONNECT_SUCCEED, inetSocketAddress);
                this.mChannelFuture = future;
                if (callback != null) {
                    callback.onSuccess();
                }
            } else {
                setConnectStatus(ConnectStatus.CONNECT_FAILED, inetSocketAddress);
                if (callback != null) {
                    callback.onFailed(future.cause());
                }
                future.channel().close();
            }
        });
    }

    public ConnectStatus connectSync(InetSocketAddress inetSocketAddress) {
        ConnectStatus currentStatus;
        if ((currentStatus = getConnectStatus()) == ConnectStatus.CONNECT_ING ||
                currentStatus == ConnectStatus.CONNECT_SUCCEED) {
            return currentStatus;
        }
        setConnectStatus(ConnectStatus.CONNECT_ING, inetSocketAddress);
        ChannelFuture channelFuture = mBootstrap.connect(inetSocketAddress).awaitUninterruptibly();
        if (channelFuture.isSuccess()) {
            setConnectStatus(ConnectStatus.CONNECT_SUCCEED, inetSocketAddress);
            this.mChannelFuture = channelFuture;
            currentStatus = ConnectStatus.CONNECT_SUCCEED;
        } else {
            Log.e(TAG, channelFuture.cause().toString());
            setConnectStatus(ConnectStatus.CONNECT_FAILED, inetSocketAddress);
            channelFuture.channel().close();
            currentStatus = ConnectStatus.CONNECT_FAILED;
        }
        return currentStatus;
    }

    @Override
    public void disConnect() {
        if (mChannelFuture != null) {
            mChannelFuture.channel().close();
            setConnectStatus(ConnectStatus.CONNECT_FAILED, null);
        }
    }

    @Override
    public void close() throws IOException {
        disConnect();
        if (mWorkGroup != null) {
            mWorkGroup.shutdownGracefully();
            mWorkGroup = null;
        }
        mBootstrap = null;
    }

    /**
     * 1.网络状态 这里并不能良好的反应出网络状态，还应该加上心跳判断
     * {@link #userEventTriggered(ChannelHandlerContext, Object)}}
     * {@link #channelActive(ChannelHandlerContext)}
     * {@link #channelInactive(ChannelHandlerContext)}
     * {@link #exceptionCaught(ChannelHandlerContext, Throwable)}
     * <p>
     * 2.接收到数据后，唤醒发送线程
     */
    private class BaseHandler extends ChannelInboundHandlerAdapter {

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("BaseHandler channelActive");
            setConnectStatus(ConnectStatus.CONNECT_SUCCEED);
            super.channelActive(ctx);
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("BaseHandler channelInactive");
            setConnectStatus(ConnectStatus.CONNECT_FAILED, null);
            super.channelInactive(ctx);
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("BaseHandler channelRead：" + msg.toString());
            afterReceive((PacketResponse) msg); //解码器返回类型
            super.channelRead(ctx, msg);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            System.out.println("exceptionCaught");
            ctx.close();
            setConnectStatus(ConnectStatus.CONNECT_FAILED, null);
            super.exceptionCaught(ctx, cause);
        }

        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            super.userEventTriggered(ctx, evt);
            if (evt instanceof IdleStateEvent) {
                switch (((IdleStateEvent) evt).state()) {
                    case WRITER_IDLE:
                        writeIdle(ctx);
                        break;
                    case READER_IDLE:
                        readIdle(ctx);
                        break;
                    case ALL_IDLE:
                        allIdle(ctx);
                        break;
                        default:
                }
            }
        }
    }

    /*======================================================================*/

    /**
     * @return
     * @see io.netty.channel.ChannelInboundHandlerAdapter
     * @see io.netty.channel.SimpleChannelInboundHandler
     */
    public Class<? super ChannelHandler>[] getHandles() {
        return null;
    }

    /**
     * @return
     * @see io.netty.handler.codec.MessageToByteEncoder
     */
    public abstract Class<? super ChannelHandler>[] getEncodes();

    /**
     * @return
     * @see io.netty.handler.codec.ByteToMessageDecoder
     */
    public abstract Class<? super ChannelHandler>[] getDecodes();

    protected boolean beforeSend(PacketRequest packetRequest) {
        return false;
    }

    protected abstract PacketResponse afterSendAndWait(PacketRequest packetRequest, int timeOutMillis) throws TimeoutException,
            InterruptedException, RejectedExecutionException;

    protected abstract void afterReceive(PacketResponse msg);

    protected int getConnectTimeout() {
        return CONNECT_TIMEOUT_MILLIS;
    }

    protected int getReadTimeout() {
        return READ_TIMEOUT_SECONDS;
    }

    protected int getWriteTimeout() {
        return WRITE_TIMEOUT_SECONDS;
    }


    protected int getAllIdleTimeout() {
        return ALL_IDLE_TIMEOUT_SECONDS;
    }

    /**
     * @param ctx
     * @see #getAllIdleTimeout()
     */
    protected void allIdle(ChannelHandlerContext ctx) {

    }

    /**
     * @param ctx
     * @see #getReadTimeout()
     */
    protected void readIdle(ChannelHandlerContext ctx) {

    }

    /**
     * @param ctx
     * @see #getWriteTimeout()
     */
    protected void writeIdle(ChannelHandlerContext ctx) {

    }


    protected void setConnectStatus(ConnectStatus connectSucceed, InetSocketAddress inetSocketAddress) {
        mConnectStatus = connectSucceed;
        if (ConnectStatus.CONNECT_SUCCEED == connectSucceed) {
            mInetSocketAddress = inetSocketAddress;
        } else if (ConnectStatus.CONNECT_FAILED == connectSucceed) {
            mInetSocketAddress = null;
        }
    }

    public InetSocketAddress getInetSocketAddress() {
        return mInetSocketAddress;
    }

    protected void setConnectStatus(ConnectStatus connectSucceed) {
        mConnectStatus = connectSucceed;
    }

    @Override
    public ConnectStatus getConnectStatus() {
        return mConnectStatus;
    }

}
